﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Text.RegularExpressions;

namespace LSHoude.FileRoller
{
    /// <summary>
    /// Rotate file numerically.
    /// 
    /// Example: file.txt, file.0.txt, file.1.txt, ...
    /// </summary>
    public class NumericFileRoller
    {
        [Flags]
        public enum Options
        {
            /// <summary>
            /// If this option is set, the source file will be copied instead of moved.
            /// </summary>
            KeepSourceFile,
            /// <summary>
            /// If this option is set, newer file will be the one with index 0.
            /// </summary>
            ReverseOrder
        }

        #region Properties
        public FileInfo File { get; private set; }
        public DirectoryInfo ArchiveDirectory { get; private set; }
        public int NumFilesToKeep { get; private set; }
        public Options SelectedOptions { get; private set; }

        private string FilePattern
        {
            get
            {
                return string.Format(@"{0}\.([0-9]+)\.{1}", Regex.Escape(this.FileNameWithoutExtension), Regex.Escape(this.File.Extension.Replace(".", "")));
            }
        }

        public string FileNameWithoutExtension
        {
            get
            {
                return Path.GetFileNameWithoutExtension(this.File.Name);
            }
        }

        /// <summary>
        /// Return true if at least one archive file exists.
        /// </summary>
        /// <returns></returns>
        public bool HasArchiveFiles
        {
            get
            {
                return this.GetArchiveFiles().Any((f) => Regex.IsMatch(f.Name, this.FilePattern));
            }
        }

        public int LastIndex
        {
            get
            {
                return this.NumFilesToKeep - 1;
            }
        }

        #endregion Properties

        #region Constructors
        public NumericFileRoller(string filePath, int numFilesToKeep, Options options)
        {
            FileInfo file = new FileInfo(filePath);
            InitFileRoller(filePath, file.Directory.FullName, numFilesToKeep, options);
        }

        public NumericFileRoller(string filePath, string archivePath, int numFilesToKeep, Options options)
        {
            InitFileRoller(filePath, archivePath, numFilesToKeep, options);
        }

        private void InitFileRoller(string filePath, string archivePath, int numFilesToKeep, Options options)
        {
            this.File = new FileInfo(filePath);
            this.ArchiveDirectory = new DirectoryInfo(archivePath);
            this.NumFilesToKeep = numFilesToKeep;
            this.SelectedOptions = options;

            // Make sure the archive directory exits.
            if (!this.ArchiveDirectory.Exists)
            {
                this.ArchiveDirectory.Create();
            }
        }
        #endregion Constructors

        public void Roll()
        {
            // If we don't want to keep any file, we have nothing to do.
            if (this.NumFilesToKeep == 0)
            {
                return;
            }

            // If the reverse order flag is set, the newer file will have an index of 0.
            if (this.SelectedOptions.HasFlag(Options.ReverseOrder))
            {
                // Take the NumFilesToKeep newest files. Substract 1 to leave space for the new file.
                IEnumerable<FileInfo> newestFiles = GetArchiveFiles().OrderBy((f) => GetFileIndex(f.Name)).Take(this.NumFilesToKeep - 1);

                // Delete oldest files
                IEnumerable<FileInfo> oldestFiles = GetArchiveFiles().Except(newestFiles, new FileInfoFilePathEqualityComparer());
                DeleteFiles(oldestFiles);

                ReIndex(newestFiles, 1);

                ArchiveFile(0);
            }
            // If the reverse order flag is NOT set, the newer file will have an index of NumFileToKeep - 1.
            else
            {
                // If there are archive files, archive the new file at the end of them,
                // or else archive the file at index 0.
                if (this.HasArchiveFiles)
                {
                    // If archive file number has reached the limit (NumFilesToKeep), we must
                    // make space for the new file.
                    if (GetCurrentIndex() >= this.LastIndex)
                    {
                        // Take the NumFilesToKeep newest files. Substract 1 to leave space for the new file.
                        IEnumerable<FileInfo> newestFiles = GetArchiveFiles().OrderByDescending((f) => GetFileIndex(f.Name)).Take(this.NumFilesToKeep - 1).Reverse();

                        // Delete oldest files
                        IEnumerable<FileInfo> oldestFiles = GetArchiveFiles().Except(newestFiles, new FileInfoFilePathEqualityComparer());
                        DeleteFiles(oldestFiles);

                        ReIndex(newestFiles, 0);
                    }

                    ArchiveFile(GetCurrentIndex() + 1);
                }
                else
                {
                    ArchiveFile(0);
                }
            }

        }

        private void ArchiveFile(int index)
        {
            // Archive source file by adding it at the end of the archive files.
            string destFilePath = GetFilePathWithIndex(index);
            if (this.SelectedOptions.HasFlag(Options.KeepSourceFile))
            {
                this.File.CopyTo(destFilePath);
            }
            else
            {
                this.File.MoveTo(destFilePath);
            }
        }

        public string GetFileNameWithIndex(int index)
        {
            return string.Format("{0}.{1}{2}", this.FileNameWithoutExtension, index, this.File.Extension);
        }

        public string GetFilePathWithIndex(int index)
        {
            return string.Format("{0}\\{1}", this.ArchiveDirectory, this.GetFileNameWithIndex(index));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <returns></returns>
        /// <remarks>This function must be tested</remarks>
        public int GetCurrentIndex()
        {
            if (!this.HasArchiveFiles)
            {
                throw new Exception("No archive file exist. Verify if there is at least one archive file exists before trying to get current index." +
                    "You can use HasArchiveFiles to do that.");
            }

            return this.GetArchiveFiles().Max((f) =>
            {
                Match match = Regex.Match(f.Name, this.FilePattern);
                if (match.Success)
                {
                    return int.Parse(match.Groups[1].Value);
                }
                else
                {
                    return 0;
                }
            });
        }

        public int GetFileIndex(string fileName)
        {
            Match match = Regex.Match(fileName, this.FilePattern);
            if (match.Success)
            {
                return int.Parse(match.Groups[1].Value);
            }
            else
            {
                throw new Exception("This file is not an archive file. Verify if file is an archive with FileIsAnArchive before trying to get index.");
            }
        }

        public bool FileIsAnArchive(string fileName)
        {
            return Regex.IsMatch(fileName, this.FilePattern);
        }

        /// <summary>
        /// Find files matching file pattern.
        /// </summary>
        /// <returns></returns>
        private IEnumerable<FileInfo> GetArchiveFiles()
        {
            return this.ArchiveDirectory.GetFiles().Where((f) =>
            {
                return Regex.IsMatch(f.Name, this.FilePattern);
            });
        }

        private void DeleteFiles(IEnumerable<FileInfo> files)
        {
            foreach (FileInfo file in files)
            {
                file.Delete();
            }
        }

        /// <summary>
        /// Make sure the index makes a continuous sequence by renaming files.
        /// Remove gap such that 1,3,5 becomes 1,2,3.
        /// </summary>
        /// <param name="files">Files to reindex. Files must already be ordered.</param>
        private void ReIndex(IEnumerable<FileInfo> files, int startIndex)
        {
            int index = startIndex;

            // First move all files to temporary file to make sure we don't overwrite existing files.
            foreach (FileInfo file in files)
            {
                MoveToWithDates(file, file.FullName + ".tmp");
            }

            // Then reindex files.
            foreach (FileInfo file in files)
            {
                string currentFilePath = GetFilePathWithIndex(index);
                MoveToWithDates(file, currentFilePath);
                index++;
            }
        }

        private static void MoveToWithDates(FileInfo file, string newPath)
        {
            DateTime creationTime = file.CreationTime;
            DateTime lastWriteTime = file.LastWriteTime;
            DateTime lastAcessTime = file.LastAccessTime;
            file.MoveTo(newPath);
            file.CreationTime = creationTime;
            file.LastWriteTime = lastWriteTime;
            file.LastAccessTime = lastAcessTime;
        }
    }
}
